﻿using System;
using System.Collections.Specialized;
using System.Diagnostics;
using System.IO;
using System.Linq;
using Eswat.Extensions.Strings;
using Eswat.IronPython;
using Eswat.IronRuby;
using Eswat.PowerShell;
using Eswat.Razor;
using Microsoft.Practices.ServiceLocation;
using Microsoft.Practices.Unity;
using RazorEngine.Templating;

namespace Eswat.Client.Console
{
	/// <summary>
	/// The console program.
	/// </summary>
	public class Program
	{
		private TestResultsXmlFormatter _testResultsFormatter;

		/// <summary>
		/// The console program's main entry point method.
		/// </summary>
		/// <param name="args">The args.</param>
		public static void Main(string[] args)
		{
			string input = null;
			string output = null;

			foreach (var arg in args)
			{
				if (arg.StartsWith("/in:"))
				{
					input = arg.Substring(4);
				}

				if (arg.StartsWith("/out:"))
				{
					output = arg.Substring(5);
				}
			}

			if (string.IsNullOrWhiteSpace(input))
			{
				System.Console.WriteLine("Missing input directory or file");
				OnBeforeExit(null);
				return;
			}

			var program = new Program(input, output);

			program.Execute();

			OnBeforeExit(output);
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="Program"/> class.
		/// </summary>
		/// <param name="input">The input.</param>
		/// <param name="output">The output.</param>
		public Program(string input, string output)
		{
			if (string.IsNullOrWhiteSpace(output)) this._testResultsFormatter = null;
			else this._testResultsFormatter = new TestResultsXmlFormatter(output);

			this.Input = input;
			this.Output = output;
		}

		/// <summary>
		/// Gets the input.
		/// </summary>
		public string Input { get; private set; }

		/// <summary>
		/// Gets the output.
		/// </summary>
		public string Output { get; private set; }

		/// <summary>
		/// Executes this instance.
		/// </summary>
		public void Execute()
		{
			if (this._testResultsFormatter != null) this._testResultsFormatter.Open();

			RegisterDependencies(this._testResultsFormatter);

			RunTestsOnInput(this.Input, this._testResultsFormatter);

			if (this._testResultsFormatter != null) this._testResultsFormatter.Close();

		}

		/// <summary>
		/// Registers the dependencies.
		/// </summary>
		private static void RegisterDependencies(TestResultsXmlFormatter testResultsFormatter)
		{
			var container = new UnityContainer();

			var locator = new UnityServiceLocator(container);

			ServiceLocator.SetLocatorProvider(() => locator);

			DependencyManager.LoadFromConfiguration(
				testService =>
					container.RegisterInstance<ITestService>(
						testService.Name,
						testService,
						new ContainerControlledLifetimeManager()),
				testResultLoggingService =>
				{
					testResultLoggingService.Results.CollectionChanged += (object sender, NotifyCollectionChangedEventArgs e) =>
					{
						if (e.Action == NotifyCollectionChangedAction.Add)
						{
							foreach (TestResult newTestResult in e.NewItems)
							{
								if (testResultsFormatter != null) testResultsFormatter.Write(newTestResult);

								System.Console.WriteLine(newTestResult);
							}
						}
					};

					container.RegisterInstance<ITestResultLoggingService>(
						testResultLoggingService,
						new ContainerControlledLifetimeManager());
				});
		}

		/// <summary>
		/// Runs the tests on input.
		/// </summary>
		/// <param name="inputPath">The input.</param>
		/// <param name="testResultsFormatter">The test results formatter.</param>
		private static void RunTestsOnInput(string inputPath, TestResultsXmlFormatter testResultsFormatter)
		{
			var isDirectory = Directory.Exists(inputPath);

			if (isDirectory) RunTestsInDirectory(inputPath, testResultsFormatter);
			else
			{
				var isFile = File.Exists(inputPath);

				if (isFile) RunTestsInFile(inputPath, testResultsFormatter);
				else
				{
					System.Console.WriteLine("Input is not a valid directory or file");
					OnBeforeExit(null);
					return;
				}
			}
		}

		/// <summary>
		/// Runs the tests in directory.
		/// </summary>
		/// <param name="directoryPath">The directory path.</param>
		/// <param name="testResultsFormatter">The test results formatter.</param>
		private static void RunTestsInDirectory(string directoryPath, TestResultsXmlFormatter testResultsFormatter)
		{
			System.Console.WriteLine("Executing all tests in directory {0}...", directoryPath);

			// process files in current directory
			var testFiles = Directory.GetFiles(directoryPath, "*.*", SearchOption.TopDirectoryOnly);

			foreach (var testFile in testFiles) RunTestsInFile(testFile, testResultsFormatter);

			// recursive call for sub-directories
			var subDirectories = Directory.GetDirectories(directoryPath);

			foreach (var subDirectoryPath in subDirectories) RunTestsInDirectory(subDirectoryPath, testResultsFormatter);
		}

		/// <summary>
		/// Runs the tests in file.
		/// </summary>
		/// <param name="filePath">The file path.</param>
		/// <param name="testResultsFormatter">The test results formatter.</param>
		private static void RunTestsInFile(string filePath, TestResultsXmlFormatter testResultsFormatter)
		{
			var ext = Path.GetExtension(filePath);

			if (IsSupportedFileExtension(ext))
			{
				string message = "Executing all tests in file {0}...".FormatWith(filePath);

				if (testResultsFormatter != null) testResultsFormatter.OnTestStarted(filePath);

				System.Console.WriteLine(message);

				var fileContents = File.ReadAllText(filePath);
				var testContext = new TestContext<bool>(true, filePath, fileContents);

				ITestRunnerResult<bool> testResult = null;

				if (ext == ".rb") testResult = RunIronRubyTests(testContext);
				else if (ext == ".py") testResult = RunIronPythonTests(testContext);
				else if (ext == ".cshtml" || ext == ".vbhtml") testResult = RunRazorTests(testContext);
				else if (ext == ".ps1") testResult = RunPowerShellTests(testContext);

				ProcessTestRunnerResult(testResult);

				if (testResultsFormatter != null) testResultsFormatter.OnTestCompleted(filePath);
			}
			else Trace.TraceInformation("Unrecognized ESWAT file extension {0} for file {1}", ext, filePath);
		}

		private readonly static string[] supportedFileExtensions = new string[] { ".rb", ".py", ".cshtml", ".vbhtml", ".ps1" };

		/// <summary>
		/// Determines whether [is supported file extension] [the specified ext].
		/// </summary>
		/// <param name="ext">The ext.</param>
		/// <returns>
		///   <c>true</c> if [is supported file extension] [the specified ext]; otherwise, <c>false</c>.
		/// </returns>
		private static bool IsSupportedFileExtension(string ext)
		{
			var isSupportedFileExtension = supportedFileExtensions.Contains(ext);

			return isSupportedFileExtension;
		}

		/// <summary>
		/// Runs the razor tests.
		/// </summary>
		/// <param name="testContext">The test context.</param>
		/// <returns></returns>
		private static ITestRunnerResult<bool> RunRazorTests(ITestContext<bool> testContext)
		{
			var testRunner = new RazorTestRunner();
			var testResult = testRunner.Execute(testContext);

			return testResult;
		}

		/// <summary>
		/// Runs the power shell tests.
		/// </summary>
		/// <param name="testContext">The test context.</param>
		/// <returns></returns>
		private static ITestRunnerResult<bool> RunPowerShellTests(ITestContext<bool> testContext)
		{
			var testRunner = new PowerShellTestRunner();
			var testResult = testRunner.Execute(testContext);

			return testResult;
		}

		/// <summary>
		/// Runs the iron python tests.
		/// </summary>
		/// <param name="testContext">The test context.</param>
		/// <returns></returns>
		private static ITestRunnerResult<bool> RunIronPythonTests(ITestContext<bool> testContext)
		{
			var testRunner = new IronPythonTestRunner();
			var testResult = testRunner.Execute(testContext);

			return testResult;
		}

		/// <summary>
		/// Runs the iron ruby tests.
		/// </summary>
		/// <param name="testContext">The test context.</param>
		/// <returns></returns>
		private static ITestRunnerResult<bool> RunIronRubyTests(ITestContext<bool> testContext)
		{
			var testRunner = new IronRubyTestRunner();
			var testResult = testRunner.Execute(testContext);

			return testResult;
		}

		/// <summary>
		/// Processes the test runner result.
		/// </summary>
		/// <param name="testRunnerResult">The test runner result.</param>
		private static void ProcessTestRunnerResult(ITestRunnerResult<bool> testRunnerResult)
		{
			if (testRunnerResult.IsError)
			{
				WriteException(testRunnerResult.InnerException);
			}
			else
			{
				System.Console.WriteLine("Test completed.");
			}
		}

		/// <summary>
		/// Writes the exception.
		/// </summary>
		/// <param name="exception">The exception.</param>
		private static void WriteException(Exception exception)
		{
			TemporarilyChangeConsoleColors(
				ConsoleColor.Black,
				ConsoleColor.Red,
				() =>
				{
					System.Console.WriteLine("ERROR:");
					System.Console.WriteLine("--------------------------------");

					if (exception is TemplateCompilationException)
					{
						var templateCompilationException = exception as TemplateCompilationException;

						WriteException(templateCompilationException);
					}
					else
					{
						System.Console.WriteLine(exception.Message);
						System.Console.WriteLine(exception.ToString());
					}
				});
		}

		/// <summary>
		/// Writes the exception.
		/// </summary>
		/// <param name="templateCompilationException">The template compilation exception.</param>
		private static void WriteException(TemplateCompilationException templateCompilationException)
		{
			System.Console.WriteLine("Error ({0}): {1}", DateTime.Now, templateCompilationException.Message);

			foreach (var error in templateCompilationException.Errors)
			{
				System.Console.WriteLine("Compile Error {0} ({1}, {2}):", error.ErrorNumber, error.Line, error.Column);
				System.Console.WriteLine(error.ErrorText);
			}

			System.Console.WriteLine(templateCompilationException);
		}

		/// <summary>
		/// Writes the error.
		/// </summary>
		/// <param name="message">The message.</param>
		private static void WriteError(string message)
		{
			TemporarilyChangeConsoleColors(ConsoleColor.Black, ConsoleColor.Red, () => System.Console.WriteLine(message));
		}

		/// <summary>
		/// Temporarilies the change console colors.
		/// </summary>
		/// <param name="background">The background.</param>
		/// <param name="foreground">The foreground.</param>
		/// <param name="temporaryAction">The temporary action.</param>
		private static void TemporarilyChangeConsoleColors(
			ConsoleColor background,
			ConsoleColor foreground,
			Action temporaryAction)
		{
			var oldBackground = System.Console.BackgroundColor;
			var oldForeground = System.Console.ForegroundColor;

			System.Console.BackgroundColor = background;
			System.Console.ForegroundColor = foreground;

			temporaryAction();

			System.Console.BackgroundColor = oldBackground;
			System.Console.ForegroundColor = oldForeground;
		}

		/// <summary>
		/// Called when [before exit].
		/// </summary>
		[Conditional("DEBUG")]
		private static void OnBeforeExit(string output)
		{
			if (File.Exists(output)) Process.Start(output);

			System.Console.WriteLine("Press any key to exit...");
			System.Console.ReadLine();
		}
	}
}
